﻿using System;
using System.Reflection;
using Kokomo.Environment.Services;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Threading;
using System.Diagnostics;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;
using Kokomo.Environment.Hosting.Services;
using Kokomo.Environment.Hosting.Settings;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.ComponentModel.Design;
using System.IO;
using Kokomo.Environment.Extensibility;

namespace Kokomo.Environment.Hosting
{
	/// <summary>
	/// Implements functionality for an application that hosts editors.
	/// </summary>
	/// <remarks>
	/// This class initializes composition and environment services,
	/// and displays the main application window provided by
	/// <see cref="CreateMainWindow"/>.
	/// <para>
	/// If <see cref="IsCompositionEnabled"/> is <c>true</c> (the default),
	/// then composition is initialized by <see cref="InitializeComposition"/>.
	/// The default implementation creates an assembly catalog containing
	/// assemblies in <see cref="ExtensionDirectoryName"/> directory,
	/// which defaults to <see cref="DefaultExtensionDirectoryName"/>.
	/// Services are initialized by <see cref="InitializeServices"/>,
	/// then editors and document templates are initialized by
	/// <see cref="RegisterEditors"/> and <see cref="RegisterDocumentTemplates()"/>,
	/// respectively.  These methods search for all extensions implementing either
	/// the <see cref="DocumentEditor"/> or <see cref="DelegatedDocumentTemplate"/>
	/// contracts.
	/// </para>
	/// </remarks>
	public class EditorApplication : Application
	{
		/// <summary>
		/// The default directory to scan for extension assemblies.
		/// </summary>
		public static readonly string DefaultExtensionDirectoryName = "Extensions";

		/// <summary>
		/// Initializes a new instance of <see cref="EditorApplication"/>.
		/// </summary>
		public EditorApplication()
		{
			this.DispatcherUnhandledException += new DispatcherUnhandledExceptionEventHandler(App_DispatcherUnhandledException);
			this.IsCompositionEnabled = true;
			this.ExtensionDirectoryName = Path.Combine(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName), DefaultExtensionDirectoryName);
		}

		/// <summary>
		/// The name of the directory containing extension assemblies.
		/// </summary>
		/// <remarks>
		/// This property defaults to <see cref="DefaultExtensionDirectoryName"/>.
		/// </remarks>
		public string ExtensionDirectoryName { get; private set; }
		internal EditorApplicationViewModel ViewModel { get; private set; }

		/// <summary>
		/// Performs application startup.
		/// </summary>
		/// <param name="e"><see cref="StartupEventArgs"/></param>
		/// <remarks>
		/// See the remarks under <see cref="EditorApplication"/> for
		/// initialization details.
		/// </remarks>
		/// <seealso cref="EditorApplication"/>
		protected override void OnStartup(StartupEventArgs e)
		{
			base.OnStartup(e);

			if (this.IsCompositionEnabled)
			{
				this.InitializeComposition();
			}
			InitializeServices();

			this._tools = new Lazy<IEnumerable<ITool>>(this.LoadTools);

			RegisterEditors();
			RegisterDocumentTemplates();

			EditorApplicationViewModel vm = new EditorApplicationViewModel(this);
			this.ViewModel = vm;

			{
				GeneralSettings general = this.SettingsManager.GetSettings<GeneralSettings>();
				general.IsInStartup = false;
				general.Save();
			}

			EditorHostWindow window = this.CreateMainWindow();
			window.DataContext = this.ViewModel;
			this.MainWindow = window;

			this.MainWindow.Show();
			this.MainWindow.Closing += new System.ComponentModel.CancelEventHandler(MainWindow_Closing);

			this.ViewModel.ReopenDocumentsFromLastSession();
		}

		void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
		{
			DocumentViewModel[] docs = this.DocumentManager.DocumentViewModels.ToArray();
			foreach (var document in docs)
			{
				document.Close();
			}
		}

		/// <summary>
		/// Creates the main application window.
		/// </summary>
		/// <returns>An instance of <see cref="EditorHostWindow"/> to display.</returns>
		/// <remarks>
		/// This implementation returns an instance of <see cref="EditorHostWindow"/>.
		/// Derived classes may override this method and return an instance of a
		/// derived type.
		/// </remarks>
		protected virtual EditorHostWindow CreateMainWindow()
		{
			return new EditorHostWindow();
		}

		/// <summary>
		/// Performs application shutdown.
		/// </summary>
		/// <param name="e"><see cref="ExitEventArgs"/></param>
		/// <remarks>
		/// This implementation records a list of open documents
		/// and saves all settings using <see cref="ISettingsManager"/>.
		/// </remarks>
		protected override void OnExit(ExitEventArgs e)
		{
			base.OnExit(e);

			GeneralSettings generalSettings = this.SettingsManager.GetSettings<GeneralSettings>();

			if (generalSettings.ReloadOpenDocumentsAtStartup)
			{
				List<string> documentList = new List<string>(this.DocumentManager.DocumentViewModels.Count());
				foreach (var document in this.DocumentManager.DocumentViewModels)
				{
					if (!string.IsNullOrEmpty(document.FileName))
					{
						documentList.Add(document.FileName);
					}
				}
				generalSettings.OpenDocumentList = documentList.ToArray();
			}

			generalSettings.IsRunning = false;
			generalSettings.Save();

			this.SettingsManager.SaveAllSettings();
		}

		#region Services
		private const string HostServicesContractNameConst = "HostServices";
		/// <summary>
		/// The name of the contract for the <see cref="IServiceProvider"/>
		/// providing environment services.
		/// </summary>
		public static readonly string HostServicesContractName = HostServicesContractNameConst;

		private ServiceContainer _serviceContainer;
		/// <summary>
		/// Gets the environment service provider.
		/// </summary>
		public IServiceContainer Services { get { return this._serviceContainer; } }

		/// <summary>
		/// Gets a value indicating whether to enable extensions via composition.
		/// </summary>
		/// <remarks>
		/// <see cref="OnStartup"/> checks this property before calling <see cref="InitializeComposition"/>,
		/// </remarks>
		/// <seealso cref="InitializeComposition"/>
		public bool IsCompositionEnabled { get; protected set; }
		/// <summary>
		/// Gets the composition container.
		/// </summary>
		public CompositionContainer Container { get; private set; }
		/// <summary>
		/// Initializes composition.
		/// </summary>
		/// <remarks>
		/// This method creates a catalog of assemblies in
		/// <see cref="ExtensionDirectoryName"/> and creates
		/// the composition container.
		/// </remarks>
		/// <seealso cref="Container"/>
		protected virtual void InitializeComposition()
		{
			AggregateCatalog catalog = new AggregateCatalog();

			if (Directory.Exists(ExtensionDirectoryName))
			{
				string[] directoryNames = Directory.GetDirectories(this.ExtensionDirectoryName);
				foreach (var directoryName in directoryNames)
				{
					string extensionName = Path.GetFileName(directoryName);
					DirectoryCatalog extensionCatalog = new DirectoryCatalog(Path.Combine(this.ExtensionDirectoryName, extensionName), extensionName + ".dll");
					catalog.Catalogs.Add(extensionCatalog);
				}
				//catalog = new AggregateCatalog(catalog, extensionCatalog);
			}

			CompositionContainer container = new CompositionContainer(catalog);
			this.Container = container;
			container.ComposeExportedValue(HostServicesContractName, this.Services);
			container.ComposeExportedValue(this.UiService);
			container.ComposeExportedValue(this.DocumentManager);
			container.ComposeExportedValue(this.SettingsManager);
		}

		internal IDocumentManager DocumentManager { get; private set; }
		internal IDocumentTemplateManager DocumentTemplateManager { get; private set; }
		internal IImageProvider ImageManager { get; private set; }
		internal IUiService UiService { get; private set; }
		internal ISettingsManager SettingsManager { get; private set; }

		protected internal EditorManager EditorManager { get; private set; }
		internal RecentDocumentService RecentDocumentService { get; private set; }
		internal AutoSaveService AutoSaveService { get; private set; }

		/// <summary>
		/// Initializes environment services.
		/// </summary>
		protected virtual void InitializeServices()
		{
			Assembly editorAssembly = typeof(EditorApplication).Assembly;

			this.DocumentManager = new DocumentManager(this);
			this.SettingsManager = new SettingsManager(this);
			this.EditorManager = new EditorManager();
			this.AutoSaveService = new AutoSaveService(this);
			this.RecentDocumentService = new RecentDocumentService(this);
			this.DocumentTemplateManager = new DocumentTemplateManager(this);
			this.UiService = new UiService();
			ImageManager imageManager = new ImageManager(editorAssembly);
			this.ImageManager = imageManager;

			this._serviceContainer = new ServiceContainer();
			this.Services.AddService(typeof(IDocumentManager), this.DocumentManager);
			this.Services.AddService(typeof(IUiService), this.UiService);
			this.Services.AddService(typeof(ISettingsManager), this.SettingsManager);
			this.Services.AddService(typeof(IImageProvider), imageManager);

			// File
			imageManager.AddImageResource(StandardImageKeys.NewDocument, "images/NewDocumentHS.png");
			imageManager.AddImageResource(StandardImageKeys.OpenDocument, "images/openHS.png");
			imageManager.AddImageResource(StandardImageKeys.SaveDocument, "images/saveHS.png");
			imageManager.AddImageResource(StandardImageKeys.SaveAllDocuments, "images/SaveAllHS.png");
			// Edit
			imageManager.AddImageResource(StandardImageKeys.Cut, "images/cutHS.png");
			imageManager.AddImageResource(StandardImageKeys.Copy, "images/copyHS.png");
			imageManager.AddImageResource(StandardImageKeys.Paste, "images/pasteHS.png");
			// Navigation
			imageManager.AddImageResource(StandardImageKeys.Back, "images/back.png");
			imageManager.AddImageResource(StandardImageKeys.Forward, "images/forward.png");
			// Window
			imageManager.AddImageResource(StandardImageKeys.NewWindow, "images/NewWindow.png");
			// Help
			imageManager.AddImageResource(StandardImageKeys.Help, "images/help.png");
		}
		#endregion

		#region Editors
		/// <summary>
		/// Registers a document editor.
		/// </summary>
		/// <param name="editor">The <see cref="DocumentEditor"/> to register.</param>
		/// <exception cref="ArgumentNullException"><paramref name="editor"/> is <c>null</c>.</exception>
		/// <remarks>
		/// Registering an editor allows the user to create and open documents
		/// supported by this editor.
		/// </remarks>
		/// <seealso cref="DocumentEditor"/>
		protected virtual void RegisterEditor(
			DocumentEditor editor
			)
		{
			if (editor == null) throw new ArgumentNullException("editor");

			this.EditorManager.RegisterEditor(editor);
		}
		/// <summary>
		/// Registers all available <see cref="DocumentEditor"/>s.
		/// </summary>
		/// <remarks>
		/// This implementation gets all <see cref="DocumentEditor"/> extensions
		/// and registers them.
		/// </remarks>
		/// <seealso cref="RegisterEditor"/>
		protected virtual void RegisterEditors()
		{
			if (!this.IsCompositionEnabled) return;

			// Editors
			IEnumerable<Lazy<DocumentEditor>> editors = this.Container.GetExports<DocumentEditor>();
			foreach (var editor in editors)
			{
				try
				{
					this.RegisterEditor(editor.Value);
				}
				catch (Exception ex)
				{
					// TODO: Log editor exception
				}
			}

			//editorManager.RegisterEditor(new RegexDocumentEditor(services));
			// UNDONE: Removed from this solution
			//editorManager.RegisterEditor(new AssemblyDocumentEditor(services));
			//editorManager.RegisterEditor(new ImageDocumentEditor(services));
		}
		/// <summary>
		/// Registers a <see cref="DocumentTemplate"/>.
		/// </summary>
		/// <param name="template">The <see cref="DocumentTemplate"/> to register.</param>
		/// <exception cref="ArgumentNullException"><paramref name="template"/> is <c>null</c>.</exception>
		/// <remarks>
		/// Registering a <see cref="DocumentTemplate"/> allows the user to create
		/// new documents using the template.
		/// </remarks>
		protected void RegisterDocumentTemplate(DocumentTemplate template)
		{
			if (template == null) throw new ArgumentNullException("template");

			this.DocumentTemplateManager.Templates.Add(template);
			//ComposablePart editorPart = AttributedModelServices.CreatePart(editor);
			//foreach (var export in editorPart.ExportDefinitions)
			//{
			//    if (export.ContractName == DelegatedDocumentTemplate.InstantiateDelegateContractName)
			//    {
			//        IDocumentTemplateMetadata templateMetadata = AttributedModelServices.GetMetadataView<IDocumentTemplateMetadata>(export.Metadata);
			//        export.
			//        DelegatedDocumentTemplate template = new DelegatedDocumentTemplate(templateMetadata.Title, export);
			//        this.DocumentTemplateManager.Templates.Add(template);
			//    }
			//}
		}
		/// <summary>
		/// Registers all available <see cref="DocumentTemplate"/>s.
		/// </summary>
		/// <remarks>
		/// This implementation gets all <see cref="DocumentEditor"/> extensions
		/// and registers them.
		/// </remarks>
		/// <seealso cref="RegisterDocumentTemplate"/>
		protected virtual void RegisterDocumentTemplates()
		{
			foreach (var editor in this.EditorManager.GetEditors())
			{
				IEnumerable<DocumentTemplate> templates = editor.GetTemplates();
				if (templates != null)
				{
					foreach (var template in templates)
					{
						this.RegisterDocumentTemplate(template);
					}
				}
			}

			if (!this.IsCompositionEnabled) return;

			// Templates
			IEnumerable<DocumentTemplate> templateExtensions = this.Container.GetExportedValues<DocumentTemplate>();
			foreach (var template in templateExtensions)
			{
				if (template != null)
					this.RegisterDocumentTemplate(template);
			}
			IEnumerable<Lazy<Func<DocumentViewModel>, IDocumentTemplateMetadata>> templateDelegates = this.Container.GetExports<Func<DocumentViewModel>, IDocumentTemplateMetadata>(DelegatedDocumentTemplate.ContractName);
			foreach (var templateDelegate in templateDelegates)
			{
				DelegatedDocumentTemplate template = new DelegatedDocumentTemplate(
					templateDelegate.Metadata.Title,
					templateDelegate.Value
					);
				this.RegisterDocumentTemplate(template);
			}
		}

		public DocumentEditor DefaultEditor { get; protected set; }

		public virtual DocumentEditor DetermineEditor(string documentFileName)
		{
			DocumentEditor editor = this.EditorManager.SuggestEditor(documentFileName);
			if (editor == null)
				editor = this.DefaultEditor;

			return editor;
		}
		#endregion
		#region Documents
		/// <summary>
		/// Creates the initial document.
		/// </summary>
		/// <remarks>
		/// When the application starts up and there are no documents to reopen
		/// from the last session, this method is called.
		/// The default implementation calls <see cref="EditorApplicationViewModel.CreateNewDocument"/>.
		/// </remarks>
		public virtual void CreateInitialDocument()
		{
			this.ViewModel.CreateNewDocument(null);
		}
		/// <summary>
		/// Creates a new document.
		/// </summary>
		/// <returns>The new <see cref="DocumentViewModel"/> instance, or <c>null</c>.</returns>
		/// <remarks>
		/// This method is called when the user activates
		/// <see cref="EditorApplicationViewModel.NewDocumentCommand"/>.
		/// The default implementation returns <c>null</c>.
		/// </remarks>
		public virtual DocumentViewModel CreateNewDocument()
		{
			if (this.DefaultEditor != null)
				return this.DefaultEditor.CreateNew();
			else
				return null;
		}
		/// <summary>
		/// Allows the user to browse for a document to open.
		/// </summary>
		/// <remarks>
		/// This method builds a list of all supported file types.
		/// </remarks>
		/// <seealso cref="DocumentEditor.SupportedFileTypes"/>
		public void OpenDocument()
		{
			var fileTypes = this.EditorManager.GetSupportedFileTypes().ToArray();
			string[] fileNames = this.UiService.ShowOpenFileDialog(new FileDialogOptions()
			{
				FileTypes = fileTypes,
				Title = UiString.FromString(Messages.OpenDocumentTitle),
				AllowMultiple = true
			}).FileNames;

			if (fileNames.Length == 0) return;

			// Close initial document
			if (
				(this.DocumentManager.DocumentViewModels.Count() == 1)
				)
			{
				DocumentViewModel doc = this.DocumentManager.DocumentViewModels.First();
				if (
					(doc.FileName == null)
					&& (!doc.IsDirty)
					)
				{
					doc.Close();
				}
			}

			DocumentViewModel lastDoc = null;
			foreach (var fileName in fileNames)
			{
				lastDoc = this.OpenDocumentFromFile(fileName);
			}

			if (lastDoc != null) this.DocumentManager.ActiveDocumentViewModel = lastDoc;
		}
		/// <summary>
		/// Opens a document from a file.
		/// </summary>
		/// <param name="fileName">The file to open.</param>
		/// <exception cref="ArgumentNullException"><paramref name="fileName"/> is <c>null</c>.</exception>
		/// <returns>A new instance of <see cref="DocumentViewModel"/>
		/// representing <paramref name="fileName"/>.
		/// </returns>
		/// <remarks>
		/// If an error occurs, this method returns an instance of
		/// <see cref="ErrorDocumentViewModel"/>
		/// </remarks>
		public DocumentViewModel OpenDocumentFromFile(string fileName)
		{
			if (fileName == null) throw new ArgumentNullException("fileName");

			DocumentViewModel documentViewModel = null;
			try
			{
				documentViewModel = this.DocumentManager.OpenDocumentFromFile(fileName);
				this.RecentDocumentService.OnOpenDocument(fileName);
			}
			catch (Exception ex)
			{
				documentViewModel = this.DocumentManager.CreateAndShowErrorDocumentViewModel(
					UiString.FromString(Messages.DocumentOpenErrorMainCaption),
					UiString.FromString(ex.Message),
					ex,
					fileName);
			}
			return documentViewModel;
		}

		#endregion
		#region Tools
		private Lazy<IEnumerable<ITool>> _tools;
		public IEnumerable<ITool> Tools { get { return this._tools.Value; } }
		protected virtual IEnumerable<ITool> LoadTools()
		{
			List<ITool> tools = new List<ITool>();
			if (this.IsCompositionEnabled)
			{
				IEnumerable<Lazy<ITool>> toolExports = this.Container.GetExports<ITool>();
				foreach (var toolExport in toolExports)
				{
					try
					{
						ITool tool = toolExport.Value;
						tools.Add(tool);
					}
					catch { 
						// TODO: Log tool exception
					}
				}
			}

			return tools;
		}
		#endregion

		#region Help
		private void ShowAboutView()
		{
			Assembly appAssembly = this.GetType().Assembly;
		}
		#endregion

		#region Error Handling
		/// <summary>
		/// Called when an exception is thrown and nothandled.
		/// </summary>
		/// <param name="e">The arguments to the event.</param>
		/// <remarks>
		/// The default instance shows a new instance of <see cref="ErrorDocumentViewModel"/>
		/// to the user.
		/// </remarks>
		protected virtual void OnUnhandledException(DispatcherUnhandledExceptionEventArgs e)
		{
			StackTrace stackTrace = new StackTrace(e.Exception);
			StackFrame[] stackFrames = stackTrace.GetFrames();

			DocumentViewModel errorDocumentViewModel = this.DocumentManager.CreateAndShowErrorDocumentViewModel(
				UiString.FromString(Messages.UnhandledExceptionMainCaption),
				UiString.FromString(Messages.UnhandledExceptionDetail),
				e.Exception
				);
			this.DocumentManager.ActiveDocumentViewModel = errorDocumentViewModel;
			e.Handled = true;
		}
		void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
		{
			this.OnUnhandledException(e);
		}
		#endregion
	}
}
